Churn prediction with Random Forest

In this project I decided to use Telco dataset, a dataset from the IBM Watson Analytics community, and apply Random Forest learning method to make prediction whether a customer will churn or not based on features and information about customers who churned within the last month or are still customers.

Why predicting customer churn?

Consumer churn prediction models are developed to determine which consumers are likely to churn and to encourage an effective segmentation of the customer base to enable companies to approach customers at risk of leaving with a retention strategy. Such a prediction models help small marketing budgets to be wisely utilized to minimize turnover, i.e. to maximize the return on marketing expenditure (ROMI). Customer retention has usually been found to be extremely profitable for companies because it costs five to six times more to acquire new customer than to retain an existing customer. Additionally, long-term customers are more profitable, appear to be less susceptible to aggressive marketing activities, appear to be less costly to service, and can create new referrals by positive word-of - mouth.

Consequently, even a small improvement in customer retention may yield significant returns.

Note: for references take a look at the Reference section

library(curl)
library(readr)
library(tidyr)
library(dplyr)
library(randomForest)
library(caret)
options(stringsAsFactor=TRUE)

Data

  • The data set includes information about:
    • Customers who left within the last month – the column is called Churn.
    • Services that each customer has signed up for – phone, multiple lines, internet, online security, online backup, device protection, tech support, and streaming TV and movies.
    • Customer account information – how long they’ve been a customer, contract, payment method, paperless billing, monthly charges, and total charges.
    • Demographic info about customers – gender, age range, and if they have partners and dependents.
Variable Topic
Gender Whether the customer is a male or a female
SeniorCitizen Whether the customer is a senior citizen or not (1, 0)
Partner Whether the customer has a partner or not (Yes, No)
Dependents Whether the customer has dependents or not (Yes, No)
Tenure Number of months the customer has stayed with the company
PhoneService Whether the customer has a phone service or not (Yes, No)
MultipleLines Whether the customer has multiple lines or not (Yes, No, No phone service)
InternetService Customer’s internet service provider (DSL, Fiber optic, No)
OnlineSecurity Whether the customer has online security or not (Yes, No, No internet service)
OnlineBackup Whether the customer has online backup or not (Yes, No, No internet service)
DeviceProtection Whether the customer has device protection or not (Yes, No, No internet service)
TechSupport Whether the customer has tech support or not (Yes, No, No internet service)
StreamingTV Whether the customer has streaming TV or not (Yes, No, No internet service)
StreamingMovie Whether the customer has streaming movies or not (Yes, No, No internet service)
Contract The contract term of the customer (Month-to-month, One year, Two year)
PaperlessBilling Whether the customer has paperless billing or not (Yes, No)
PaymentMethod The customer’s payment method (Electronic check, Mailed check, Bank transfer (automatic), Credit card (automatic))
MonthlyCharges The amount charged to the customer monthly
TotalCharges The total amount charged to the customer
Churn Whether the customer churned or not (Yes or No)
Note: Information collected from https://www.kaggle.com/blastchar/telco-customer-churn

Here are dimensions of our data set and first 6 rows:

Data Pre-processing

First, we need to remove rows which have at least one NA as a value:

# Drop Nas
dim(x)
[1] 7043   21
x <- x %>% drop_na()
dim(x)
[1] 7032   21

11 are removed from the data set as they held at least one NA as a value. Next, we need to prepare our data set in terms of data types. In order to use Random Forest for churn prediction, we need to make sure that our categorical are represented in numeric manner.

Therefore, all categorical variables will be converted to factors.

# Categorical variables to factors
x[c("Partner","Dependents","PhoneService","gender","MultipleLines","InternetService","OnlineSecurity","OnlineBackup","DeviceProtection","TechSupport","StreamingTV","Contract","StreamingMovies","PaperlessBilling","PaymentMethod","Churn")]<-lapply(x[c("Partner","Dependents","PhoneService","gender","MultipleLines","InternetService","OnlineSecurity","OnlineBackup","DeviceProtection","TechSupport","StreamingTV","Contract","StreamingMovies","PaperlessBilling","PaymentMethod","Churn")], as.factor)
str(x)
'data.frame':   7032 obs. of  21 variables:
 $ customerID      : chr  "7590-VHVEG" "5575-GNVDE" "3668-QPYBK" "7795-CFOCW" ...
 $ gender          : Factor w/ 2 levels "Female","Male": 1 2 2 2 1 1 2 1 1 2 ...
 $ SeniorCitizen   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ Partner         : Factor w/ 2 levels "No","Yes": 2 1 1 1 1 1 1 1 2 1 ...
 $ Dependents      : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 2 1 1 2 ...
 $ tenure          : int  1 34 2 45 2 8 22 10 28 62 ...
 $ PhoneService    : Factor w/ 2 levels "No","Yes": 1 2 2 1 2 2 2 1 2 2 ...
 $ MultipleLines   : Factor w/ 3 levels "No","No phone service",..: 2 1 1 2 1 3 3 2 3 1 ...
 $ InternetService : Factor w/ 3 levels "DSL","Fiber optic",..: 1 1 1 1 2 2 2 1 2 1 ...
 $ OnlineSecurity  : Factor w/ 3 levels "No","No internet service",..: 1 3 3 3 1 1 1 3 1 3 ...
 $ OnlineBackup    : Factor w/ 3 levels "No","No internet service",..: 3 1 3 1 1 1 3 1 1 3 ...
 $ DeviceProtection: Factor w/ 3 levels "No","No internet service",..: 1 3 1 3 1 3 1 1 3 1 ...
 $ TechSupport     : Factor w/ 3 levels "No","No internet service",..: 1 1 1 3 1 1 1 1 3 1 ...
 $ StreamingTV     : Factor w/ 3 levels "No","No internet service",..: 1 1 1 1 1 3 3 1 3 1 ...
 $ StreamingMovies : Factor w/ 3 levels "No","No internet service",..: 1 1 1 1 1 3 1 1 3 1 ...
 $ Contract        : Factor w/ 3 levels "Month-to-month",..: 1 2 1 2 1 1 1 1 1 2 ...
 $ PaperlessBilling: Factor w/ 2 levels "No","Yes": 2 1 2 1 2 2 2 1 2 1 ...
 $ PaymentMethod   : Factor w/ 4 levels "Bank transfer (automatic)",..: 3 4 4 1 3 3 2 4 3 1 ...
 $ MonthlyCharges  : num  29.9 57 53.9 42.3 70.7 ...
 $ TotalCharges    : num  29.9 1889.5 108.2 1840.8 151.7 ...
 $ Churn           : Factor w/ 2 levels "No","Yes": 1 1 2 1 2 2 1 1 2 1 ...

We aim to convert our data set into matrix with only 0s and 1s. Thus, all binomial variables can be turned to 0s and 1s in the following way:

# Binomial categorical variables to 0 and 1
x$PhoneService <- as.numeric(x$PhoneService)-1
x$Partner <-as.numeric(x$Partner)-1
x$Dependents <- as.numeric(x$Dependents)-1
x$Churn <- as.numeric(x$Churn)-1
x$gender <- as.numeric(x$gender)-1

Now it is left to convert multi-class variables 0s and 1s. Before we do it, let’s inspect our data a bit:

## Categorical variables with more than 2 categories
par(mfrow=c(5,2))
# Multiple lines
ggplot(x, aes(MultipleLines,fill=MultipleLines)) + 
  geom_bar() +
  labs(title="Multiple lines",x="",y="Count")


# Internet services
ggplot(x, aes(InternetService,fill=InternetService)) + 
  geom_bar() +
  labs(title="Internet service",x="",y="Count")


# OnlineSecurity
ggplot(x, aes(OnlineSecurity,fill=OnlineSecurity)) + 
  geom_bar() +
  labs(title="OnlineSecurity",x="",y="Count")


# OnlineBackup
ggplot(x, aes(OnlineBackup,fill=OnlineBackup)) + 
  geom_bar() +
  labs(title="OnlineBackup",x="",y="Count")


# DeviceProtection
ggplot(x, aes(DeviceProtection,fill=DeviceProtection)) + 
  geom_bar() +
  labs(title="DeviceProtection",x="",y="Count")


# TechSupport
ggplot(x, aes(TechSupport,fill=TechSupport)) + 
  geom_bar() +
  labs(title="TechSupport",x="",y="Count")


# StreamingTV
ggplot(x, aes(StreamingTV,fill=StreamingTV)) + 
  geom_bar() +
  labs(title="StreamingTV",x="",y="Count")


# StreamingMovies
ggplot(x, aes(StreamingMovies,fill=StreamingMovies)) + 
  geom_bar() +
  labs(title="StreamingMovies",x="",y="Count")


# Contract
ggplot(x, aes(Contract,fill=Contract)) + 
  geom_bar() +
  labs(title="Contract",x="",y="Count")


# PaymentMethod
ggplot(x, aes(PaymentMethod,fill=PaymentMethod)) + 
  geom_bar() +
  labs(title="PaymentMethod",x="",y="Count")

We can apply one hot encoding to our data set by using R’s base function model.matrix. In the code below, ~.+0 leads to encoding of all categorical variables without producing an intercept.

# One-hot encoding
x.mat<- model.matrix(~MultipleLines+InternetService+OnlineSecurity+OnlineBackup+DeviceProtection+TechSupport+StreamingTV+Contract+StreamingMovies+PaperlessBilling+PaymentMethod+0,data = x)

Now is our data set pre-processed:

# Creation of the final data frame with 0s and 1s
final.df<-as.data.frame(x.mat)
final.df<- cbind(x.mat,x$tenure,x$TotalCharges,x$MonthlyCharges,x$PhoneService,x$Partner,x$Dependents,x$gender,x$Churn)
colnames(final.df)[24:31]<-c("tenure","TotalCharges","MonthlyCharges","PhoneService","Partner","Dependents","gender","Churn")
head(final.df)
  MultipleLinesNo MultipleLinesNo phone service MultipleLinesYes
1               0                             1                0
2               1                             0                0
3               1                             0                0
4               0                             1                0
5               1                             0                0
6               0                             0                1
  InternetServiceFiber optic InternetServiceNo
1                          0                 0
2                          0                 0
3                          0                 0
4                          0                 0
5                          1                 0
6                          1                 0
  OnlineSecurityNo internet service OnlineSecurityYes
1                                 0                 0
2                                 0                 1
3                                 0                 1
4                                 0                 1
5                                 0                 0
6                                 0                 0
  OnlineBackupNo internet service OnlineBackupYes
1                               0               1
2                               0               0
3                               0               1
4                               0               0
5                               0               0
6                               0               0
  DeviceProtectionNo internet service DeviceProtectionYes
1                                   0                   0
2                                   0                   1
3                                   0                   0
4                                   0                   1
5                                   0                   0
6                                   0                   1
  TechSupportNo internet service TechSupportYes
1                              0              0
2                              0              0
3                              0              0
4                              0              1
5                              0              0
6                              0              0
  StreamingTVNo internet service StreamingTVYes ContractOne year
1                              0              0                0
2                              0              0                1
3                              0              0                0
4                              0              0                1
5                              0              0                0
6                              0              1                0
  ContractTwo year StreamingMoviesNo internet service
1                0                                  0
2                0                                  0
3                0                                  0
4                0                                  0
5                0                                  0
6                0                                  0
  StreamingMoviesYes PaperlessBillingYes
1                  0                   1
2                  0                   0
3                  0                   1
4                  0                   0
5                  0                   1
6                  1                   1
  PaymentMethodCredit card (automatic)
1                                    0
2                                    0
3                                    0
4                                    0
5                                    0
6                                    0
  PaymentMethodElectronic check PaymentMethodMailed check tenure
1                             1                         0      1
2                             0                         1     34
3                             0                         1      2
4                             0                         0     45
5                             1                         0      2
6                             1                         0      8
  TotalCharges MonthlyCharges PhoneService Partner Dependents
1        29.85          29.85            0       1          0
2      1889.50          56.95            1       0          0
3       108.15          53.85            1       0          0
4      1840.75          42.30            0       0          0
5       151.65          70.70            1       0          0
6       820.50          99.65            1       0          0
  gender Churn
1      0     0
2      1     0
3      1     1
4      1     0
5      0     1
6      0     1

Data partition

Now we need to split our data into test and train data. The proportion is 70:30.

# Creation of training and test data sets
index <- caret::createDataPartition(x$Churn, p = 0.7, list = F)
train <- final.df[index,]
test <- final.df[-index,]

# Partitioning test data
x_test <- as.matrix(test[,-31])  
y_test <- as.matrix(test[,31])

# Partitioning train data
x_train <- as.matrix(train[,-31])
y_train <- as.matrix(train[,31])

To train our random forest model we use function randomForest(). At this point we will not aim to fine tune our model, so we will define just two parameters,ntree and maxnodes:

  • ntree defines the number of trees to build in the forest.
  • maxnodes defines the maximum number of terminal nodes each tree in the forest can have.

Random Forest model

# Random Forest
# Training
library(randomForest)
rfModel <- randomForest(x=x_train,
                        y=factor(y_train),
                        ntree=500,
                        maxnodes=24)

Since predictions are made based on features our model was trained on, it is possible to observe importance of each feature. The more important a feature, the greater influence it exerts on predictions:

importance_features <- randomForest::importance(rfModel)
importance_features <- as.data.frame(importance_features)
importance_features$features <- row.names(importance_features)
importance_features <- importance_features[order(importance_features$MeanDecreaseGini ,decreasing = TRUE),]

library(plotly)
p<-ggplot(importance_features) +
  geom_point(aes(reorder(features,MeanDecreaseGini),MeanDecreaseGini),stat = "identity")+
  theme_minimal()+
  coord_flip()+
  labs(title="Important features",x="Features")
ggplotly(p)

As we can see from this output, the tenure feature seems to be the most important factor in making the final prediction. Factors such as InternetServiceFiber optic,TotalCharges and ContractTwo year come subsequently.

Evaluating Model

In order to evaluate our model we will take a look at accuracy, precision and recall.

# Evaluating Models
prediction_insample <- as.double(predict(rfModel, x_train)) - 1
prediction_outsample <- as.double(predict(rfModel, x_test)) - 1

Accuracy is the percentage of correct predictions out of all predictions.

# Accuracy
accu_insample <- mean(y_train == prediction_insample)
accu_outsample <- mean(y_test == prediction_outsample)
print(sprintf('In-Sample Accuracy: %0.4f', accu_insample))
[1] "In-Sample Accuracy: 0.7989"
print(sprintf('Out-Sample Accuracy: %0.4f', accu_outsample))
[1] "Out-Sample Accuracy: 0.7937"

We managed to achieve pretty good out-sample accuracy even without thorough fine-tuning our parameters. Precision is the number of true positives divided by the total number of true positives and false positives.

# Precision
prec_insample <- sum(prediction_insample & y_train) / sum(prediction_insample)
prec_outsample <- sum(prediction_outsample & y_test) / sum(prediction_outsample)
print(sprintf('In-Sample Precision: %0.4f', prec_insample))
[1] "In-Sample Precision: 0.6923"
print(sprintf('Out-Sample Precision: %0.4f', prec_outsample))
[1] "Out-Sample Precision: 0.6615"

Recall is defined as the number of true positives divided by number of true positives plus false negatives.

# Recall 
recall_insample <- sum(prediction_insample & y_train) / sum(y_train)
recall_outsample <- sum(prediction_outsample & y_test) / sum(y_test)
print(sprintf('In-Sample Recall: %0.4f', recall_insample))
[1] "In-Sample Recall: 0.4397"
print(sprintf('Out-Sample Recall: %0.4f', recall_outsample))
[1] "Out-Sample Recall: 0.4544"

Finally, in order to estimate how good our model is in comparison to the random prediction, we will inspect ROC curve and the Area Under the Curve.

library(ROCR)

pred_prob_insample <- as.double(predict(rfModel, x_train, type='prob')[,2])
pred_prob_outsample <- as.double(predict(rfModel, x_test, type='prob')[,2])
pred <- prediction(pred_prob_outsample, y_test)
perf <- performance(pred, measure = "tpr", x.measure = "fpr") 
auc <- performance(pred, measure='auc')@y.values[[1]]

{plot(perf,main=sprintf('Random Forest (AUC: %0.2f)', auc),col='darkblue',lwd=2) + grid()
abline(a = 0, b = 1, col='darkgray', lty=4, lwd=2)}

References

  • Hwang, Y. H. (2019). Hands-on data science for marketing: Improve your marketing strategies with machine learning using Python and R. Birmingham: Packt Publishing.

  • W. Verbeke, D. Martens, C. Mues, B. Baesens. Building comprehensible customer churn prediction models with advanced rule induction techniques Expert Syst. Appl., 38 (3) (2011), pp. 2354-2364.

  • M.R. Colgate, P.J. Danaher. Implementing a customer relationship strategy: the asymmetric impact of poor versus excellent execution J. Acad. Mark. Sci., 28 (3) (2000), pp. 375-387.

  • J. Ganesh, M.J. Arnold, K.E. Reynolds. Understanding the customer base of service providers: an examination of the differences between switchers and stayers.J. Mark., 64 (3) (2000), pp. 65-87.

  • D.V. den Poel, B. Larivière. Customer attrition analysis for financial services using proportional hazard models. Eur. J. Oper. Res., 157 (1) (2004), pp. 196-217.

LS0tDQp0aXRsZTogIlJhbmRvbSBGb3Jlc3QgLSBDaHVybiINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDoNCiAgICBrZWVwX21kOiB0cnVlDQogICAgdG9jOiB5ZXMNCiAgICBkZl9wcmludDogcGFnZWQNCiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdA0KICBwZGZfZG9jdW1lbnQ6DQogICAgdG9jOiB5ZXMNCi0tLQ0KDQojIENodXJuIHByZWRpY3Rpb24gd2l0aCBSYW5kb20gRm9yZXN0DQoNCmBgYHtyLGVjaG89RkFMU0UsIGZpZy5hbGlnbj0nY2VudGVyJywgb3V0LndpZHRoPSI1MCUiLGZpZy5jYXA9IkZvdG8gZnJvbSB0ZWNobm9sb2d5YWR2aWNlLmNvbSJ9DQprbml0cjo6aW5jbHVkZV9ncmFwaGljcygiR3JhcGhpY3MvY2h1cm4ucG5nIikNCmBgYA0KDQpJbiB0aGlzIHByb2plY3QgSSBkZWNpZGVkIHRvIHVzZSBUZWxjbyBkYXRhc2V0LCBhIGRhdGFzZXQgZnJvbSB0aGUgSUJNDQpXYXRzb24gQW5hbHl0aWNzIGNvbW11bml0eSwgYW5kIGFwcGx5IFJhbmRvbSBGb3Jlc3QgbGVhcm5pbmcgbWV0aG9kIHRvIG1ha2UgcHJlZGljdGlvbiB3aGV0aGVyIGEgY3VzdG9tZXIgd2lsbCBjaHVybiBvciBub3QgYmFzZWQgb24gZmVhdHVyZXMgYW5kIGluZm9ybWF0aW9uIGFib3V0IGN1c3RvbWVycyB3aG8gY2h1cm5lZCB3aXRoaW4gdGhlIGxhc3QgbW9udGggb3IgYXJlIHN0aWxsIGN1c3RvbWVycy4NCg0KIyMgV2h5IHByZWRpY3RpbmcgY3VzdG9tZXIgY2h1cm4/DQoNCjxkaXYgc3R5bGU9InRleHQtYWxpZ246IGp1c3RpZnkiPiANCg0KQ29uc3VtZXIgY2h1cm4gcHJlZGljdGlvbiBtb2RlbHMgYXJlIGRldmVsb3BlZCB0byBkZXRlcm1pbmUgd2hpY2ggY29uc3VtZXJzIGFyZSBsaWtlbHkgdG8gY2h1cm4gYW5kIHRvIGVuY291cmFnZSBhbiBlZmZlY3RpdmUgc2VnbWVudGF0aW9uIG9mIHRoZSBjdXN0b21lciBiYXNlIHRvIGVuYWJsZSBjb21wYW5pZXMgdG8gYXBwcm9hY2ggY3VzdG9tZXJzIGF0IHJpc2sgb2YgbGVhdmluZyB3aXRoIGEgcmV0ZW50aW9uIHN0cmF0ZWd5LiBTdWNoIGEgcHJlZGljdGlvbiBtb2RlbHMgaGVscCBzbWFsbCBtYXJrZXRpbmcgYnVkZ2V0cyB0byBiZSB3aXNlbHkgdXRpbGl6ZWQgdG8gbWluaW1pemUgdHVybm92ZXIsIGkuZS4gdG8gbWF4aW1pemUgdGhlIHJldHVybiBvbiBtYXJrZXRpbmcgZXhwZW5kaXR1cmUgKFJPTUkpLiBDdXN0b21lciByZXRlbnRpb24gaGFzIHVzdWFsbHkgYmVlbiBmb3VuZCB0byBiZSBleHRyZW1lbHkgcHJvZml0YWJsZSBmb3IgY29tcGFuaWVzIGJlY2F1c2UgaXQgY29zdHMgZml2ZSB0byBzaXggdGltZXMgbW9yZSB0byBhY3F1aXJlIG5ldyBjdXN0b21lciB0aGFuIHRvIHJldGFpbiBhbiBleGlzdGluZyBjdXN0b21lci4gQWRkaXRpb25hbGx5LCBsb25nLXRlcm0gY3VzdG9tZXJzIGFyZSBtb3JlIHByb2ZpdGFibGUsIGFwcGVhciB0byBiZSBsZXNzIHN1c2NlcHRpYmxlIHRvIGFnZ3Jlc3NpdmUgbWFya2V0aW5nIGFjdGl2aXRpZXMsIGFwcGVhciB0byBiZSBsZXNzIGNvc3RseSB0byBzZXJ2aWNlLCBhbmQgY2FuIGNyZWF0ZSBuZXcgcmVmZXJyYWxzIGJ5IHBvc2l0aXZlIHdvcmQtb2YgLSBtb3V0aC4NCg0KQ29uc2VxdWVudGx5LCBldmVuIGEgc21hbGwgaW1wcm92ZW1lbnQgaW4gY3VzdG9tZXIgcmV0ZW50aW9uIG1heSB5aWVsZCBzaWduaWZpY2FudCByZXR1cm5zLg0KDQoqTm90ZTogZm9yIHJlZmVyZW5jZXMgdGFrZSBhIGxvb2sgYXQgdGhlIFJlZmVyZW5jZSBzZWN0aW9uKg0KDQo8L2Rpdj4gDQoNCmBgYHtyLGVycm9yPUZBTFNFLHdhcm5pbmc9RkFMU0UsbWVzc2FnZT1GQUxTRX0NCm9wdGlvbnMoc3RyaW5nc0FzRmFjdG9yPVRSVUUpDQpsaWJyYXJ5KGN1cmwpDQpsaWJyYXJ5KHJlYWRyKQ0KbGlicmFyeSh0aWR5cikNCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KHJhbmRvbUZvcmVzdCkNCmxpYnJhcnkoY2FyZXQpDQpgYGANCg0KIyMgRGF0YQ0KDQoqIFRoZSBkYXRhIHNldCBpbmNsdWRlcyBpbmZvcm1hdGlvbiBhYm91dDogIA0KICAgICsgQ3VzdG9tZXJzIHdobyBsZWZ0IHdpdGhpbiB0aGUgbGFzdCBtb250aCDigJMgdGhlIGNvbHVtbiBpcyBjYWxsZWQgKipDaHVybioqLg0KICAgICsgU2VydmljZXMgdGhhdCBlYWNoIGN1c3RvbWVyIGhhcyBzaWduZWQgdXAgZm9yIOKAkyBwaG9uZSwgbXVsdGlwbGUgbGluZXMsIGludGVybmV0LCBvbmxpbmUgc2VjdXJpdHksIG9ubGluZSBiYWNrdXAsIGRldmljZSBwcm90ZWN0aW9uLCB0ZWNoIHN1cHBvcnQsIGFuZCBzdHJlYW1pbmcgVFYgYW5kIG1vdmllcy4NCiAgICArIEN1c3RvbWVyIGFjY291bnQgaW5mb3JtYXRpb24g4oCTIGhvdyBsb25nIHRoZXnigJl2ZSBiZWVuIGEgY3VzdG9tZXIsIGNvbnRyYWN0LCBwYXltZW50IG1ldGhvZCwgcGFwZXJsZXNzIGJpbGxpbmcsIG1vbnRobHkgY2hhcmdlcywgYW5kIHRvdGFsIGNoYXJnZXMuDQogICAgKyBEZW1vZ3JhcGhpYyBpbmZvIGFib3V0IGN1c3RvbWVycyDigJMgZ2VuZGVyLCBhZ2UgcmFuZ2UsIGFuZCBpZiB0aGV5IGhhdmUgcGFydG5lcnMgYW5kIGRlcGVuZGVudHMuDQoNCmBgYHtyIGV2YWwgPSBUUlVFLCBlY2hvID0gRkFMU0UsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2UgPSBGQUxTRX0NCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KGthYmxlRXh0cmEpDQpteXRhYmxlX3N1YiA9IGRhdGEuZnJhbWUoDQogICAgVmFyaWFibGUgPSBjKCJHZW5kZXIiLCJTZW5pb3JDaXRpemVuIiwiUGFydG5lciIsIkRlcGVuZGVudHMiLCJUZW51cmUiLCJQaG9uZVNlcnZpY2UiLCJNdWx0aXBsZUxpbmVzIiwiSW50ZXJuZXRTZXJ2aWNlIiwiT25saW5lU2VjdXJpdHkiLCJPbmxpbmVCYWNrdXAiLCJEZXZpY2VQcm90ZWN0aW9uIiwiVGVjaFN1cHBvcnQiLCJTdHJlYW1pbmdUViIsIlN0cmVhbWluZ01vdmllIiwiQ29udHJhY3QiLCJQYXBlcmxlc3NCaWxsaW5nIiwiUGF5bWVudE1ldGhvZCIsIk1vbnRobHlDaGFyZ2VzIiwiVG90YWxDaGFyZ2VzIiwiQ2h1cm4iKSwNCiAgICBUb3BpYyA9IGMoIldoZXRoZXIgdGhlIGN1c3RvbWVyIGlzIGEgbWFsZSBvciBhIGZlbWFsZSIsDQogICAgICAgICAgICAgIldoZXRoZXIgdGhlIGN1c3RvbWVyIGlzIGEgc2VuaW9yIGNpdGl6ZW4gb3Igbm90ICgxLCAwKSIsDQogICAgICAgICAgICAgIldoZXRoZXIgdGhlIGN1c3RvbWVyIGhhcyBhIHBhcnRuZXIgb3Igbm90IChZZXMsIE5vKSIsDQogICAgICAgICAgICAgIldoZXRoZXIgdGhlIGN1c3RvbWVyIGhhcyBkZXBlbmRlbnRzIG9yIG5vdCAoWWVzLCBObykiLA0KICAgICAgICAgICAgICJOdW1iZXIgb2YgbW9udGhzIHRoZSBjdXN0b21lciBoYXMgc3RheWVkIHdpdGggdGhlIGNvbXBhbnkiLA0KICAgICAgICAgICAgICJXaGV0aGVyIHRoZSBjdXN0b21lciBoYXMgYSBwaG9uZSBzZXJ2aWNlIG9yIG5vdCAoWWVzLCBObykiLA0KICAgICAgICAgICAgICJXaGV0aGVyIHRoZSBjdXN0b21lciBoYXMgbXVsdGlwbGUgbGluZXMgb3Igbm90IChZZXMsIE5vLCBObyBwaG9uZSBzZXJ2aWNlKSIsDQogICAgICAgICAgICAgIkN1c3RvbWVy4oCZcyBpbnRlcm5ldCBzZXJ2aWNlIHByb3ZpZGVyIChEU0wsIEZpYmVyIG9wdGljLCBObykiLA0KICAgICAgICAgICAgICJXaGV0aGVyIHRoZSBjdXN0b21lciBoYXMgb25saW5lIHNlY3VyaXR5IG9yIG5vdCAoWWVzLCBObywgTm8gaW50ZXJuZXQgc2VydmljZSkiLA0KICAgICAgICAgICAgICJXaGV0aGVyIHRoZSBjdXN0b21lciBoYXMgb25saW5lIGJhY2t1cCBvciBub3QgKFllcywgTm8sIE5vIGludGVybmV0IHNlcnZpY2UpIiwNCiAgICAgICAgICAgICAiV2hldGhlciB0aGUgY3VzdG9tZXIgaGFzIGRldmljZSBwcm90ZWN0aW9uIG9yIG5vdCAoWWVzLCBObywgTm8gaW50ZXJuZXQgc2VydmljZSkiLA0KICAgICAgICAgICAgICJXaGV0aGVyIHRoZSBjdXN0b21lciBoYXMgdGVjaCBzdXBwb3J0IG9yIG5vdCAoWWVzLCBObywgTm8gaW50ZXJuZXQgc2VydmljZSkiLA0KICAgICAgICAgICAgICJXaGV0aGVyIHRoZSBjdXN0b21lciBoYXMgc3RyZWFtaW5nIFRWIG9yIG5vdCAoWWVzLCBObywgTm8gaW50ZXJuZXQgc2VydmljZSkiLA0KICAgICAgICAgICAgICJXaGV0aGVyIHRoZSBjdXN0b21lciBoYXMgc3RyZWFtaW5nIG1vdmllcyBvciBub3QgKFllcywgTm8sIE5vIGludGVybmV0IHNlcnZpY2UpIiwNCiAgICAgICAgICAgICAiVGhlIGNvbnRyYWN0IHRlcm0gb2YgdGhlIGN1c3RvbWVyIChNb250aC10by1tb250aCwgT25lIHllYXIsIFR3byB5ZWFyKSIsDQogICAgICAgICAgICAgIldoZXRoZXIgdGhlIGN1c3RvbWVyIGhhcyBwYXBlcmxlc3MgYmlsbGluZyBvciBub3QgKFllcywgTm8pIiwNCiAgICAgICAgICAgICAiVGhlIGN1c3RvbWVy4oCZcyBwYXltZW50IG1ldGhvZCAoRWxlY3Ryb25pYyBjaGVjaywgTWFpbGVkIGNoZWNrLCBCYW5rIHRyYW5zZmVyIChhdXRvbWF0aWMpLCBDcmVkaXQgY2FyZCAoYXV0b21hdGljKSkiLA0KICAgICAgICAgICAgICJUaGUgYW1vdW50IGNoYXJnZWQgdG8gdGhlIGN1c3RvbWVyIG1vbnRobHkiLA0KICAgICAgICAgICAgICJUaGUgdG90YWwgYW1vdW50IGNoYXJnZWQgdG8gdGhlIGN1c3RvbWVyIiwNCiAgICAgICAgICAgICAiV2hldGhlciB0aGUgY3VzdG9tZXIgY2h1cm5lZCBvciBub3QgKFllcyBvciBObykiDQogICAgICAgICAgICAgKSkNCg0KbXl0YWJsZV9zdWIgJT4lIGthYmxlKGVzY2FwZSA9IFQpICU+JQ0KICBrYWJsZV9wYXBlcihjKCJob3ZlciIpLCBmdWxsX3dpZHRoID0gRikgJT4lDQogIGZvb3Rub3RlKGdlbmVyYWwgPSAiIEluZm9ybWF0aW9uIGNvbGxlY3RlZCBmcm9tIGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vYmxhc3RjaGFyL3RlbGNvLWN1c3RvbWVyLWNodXJuIiwNCiAgICAgICAgICAgZ2VuZXJhbF90aXRsZSA9ICJOb3RlOiAiLCANCiAgICAgICAgICAgZm9vdG5vdGVfYXNfY2h1bmsgPSBULCB0aXRsZV9mb3JtYXQgPSBjKCJpdGFsaWMiKQ0KICAgICAgICAgICApIA0KYGBgDQoNCkhlcmUgYXJlIGRpbWVuc2lvbnMgb2Ygb3VyIGRhdGEgc2V0IGFuZCBmaXJzdCA2IHJvd3M6DQoNCmBgYHtyLGVycm9yPUZBTFNFLHdhcm5pbmc9RkFMU0UsbWVzc2FnZT1GQUxTRSxlY2hvPUZBTFNFfQ0KIyBSZWFkIGluIGRhdGENCnggPC0gcmVhZC5jc3YoY3VybCgiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3RyZXNlbGxlLXN5c3RlbXMvY3VzdG9tZXJfY2h1cm5fYW5hbHlzaXMvbWFzdGVyL1dBX0ZuLVVzZUNfLVRlbGNvLUN1c3RvbWVyLUNodXJuLmNzdiIpKQ0KaGVhZCh4KQ0KYGBgDQoNCiMjIERhdGEgUHJlLXByb2Nlc3NpbmcNCg0KRmlyc3QsIHdlIG5lZWQgdG8gcmVtb3ZlIHJvd3Mgd2hpY2ggaGF2ZSBhdCBsZWFzdCBvbmUgTkEgYXMgYSB2YWx1ZToNCg0KYGBge3IsZXJyb3I9RkFMU0Usd2FybmluZz1GQUxTRSxtZXNzYWdlPUZBTFNFfQ0KIyBEcm9wIE5hcw0KZGltKHgpDQp4IDwtIHggJT4lIGRyb3BfbmEoKQ0KZGltKHgpDQpgYGANCg0KMTEgYXJlIHJlbW92ZWQgZnJvbSB0aGUgZGF0YSBzZXQgYXMgdGhleSBoZWxkIGF0IGxlYXN0IG9uZSBOQSBhcyBhIHZhbHVlLiBOZXh0LCB3ZSBuZWVkIHRvIHByZXBhcmUgb3VyIGRhdGEgc2V0IGluIHRlcm1zIG9mIGRhdGEgdHlwZXMuIEluIG9yZGVyIHRvIHVzZSBSYW5kb20gRm9yZXN0IGZvciBjaHVybiBwcmVkaWN0aW9uLCB3ZSBuZWVkIHRvIG1ha2Ugc3VyZSB0aGF0IG91ciBjYXRlZ29yaWNhbCBhcmUgcmVwcmVzZW50ZWQgaW4gbnVtZXJpYyBtYW5uZXIuDQoNClRoZXJlZm9yZSwgYWxsIGNhdGVnb3JpY2FsIHZhcmlhYmxlcyB3aWxsIGJlIGNvbnZlcnRlZCB0byBmYWN0b3JzLg0KDQpgYGB7cixlcnJvcj1GQUxTRSxtZXNzYWdlPUZBTFNFLHdhcm5pbmc9RkFMU0V9DQojIENhdGVnb3JpY2FsIHZhcmlhYmxlcyB0byBmYWN0b3JzDQp4W2MoIlBhcnRuZXIiLCJEZXBlbmRlbnRzIiwiUGhvbmVTZXJ2aWNlIiwiZ2VuZGVyIiwiTXVsdGlwbGVMaW5lcyIsIkludGVybmV0U2VydmljZSIsIk9ubGluZVNlY3VyaXR5IiwiT25saW5lQmFja3VwIiwiRGV2aWNlUHJvdGVjdGlvbiIsIlRlY2hTdXBwb3J0IiwiU3RyZWFtaW5nVFYiLCJDb250cmFjdCIsIlN0cmVhbWluZ01vdmllcyIsIlBhcGVybGVzc0JpbGxpbmciLCJQYXltZW50TWV0aG9kIiwiQ2h1cm4iKV08LWxhcHBseSh4W2MoIlBhcnRuZXIiLCJEZXBlbmRlbnRzIiwiUGhvbmVTZXJ2aWNlIiwiZ2VuZGVyIiwiTXVsdGlwbGVMaW5lcyIsIkludGVybmV0U2VydmljZSIsIk9ubGluZVNlY3VyaXR5IiwiT25saW5lQmFja3VwIiwiRGV2aWNlUHJvdGVjdGlvbiIsIlRlY2hTdXBwb3J0IiwiU3RyZWFtaW5nVFYiLCJDb250cmFjdCIsIlN0cmVhbWluZ01vdmllcyIsIlBhcGVybGVzc0JpbGxpbmciLCJQYXltZW50TWV0aG9kIiwiQ2h1cm4iKV0sIGFzLmZhY3RvcikNCnN0cih4KQ0KYGBgDQoNCldlIGFpbSB0byBjb252ZXJ0IG91ciBkYXRhIHNldCBpbnRvIG1hdHJpeCB3aXRoIG9ubHkgMHMgYW5kIDFzLiBUaHVzLCBhbGwgYmlub21pYWwgdmFyaWFibGVzIGNhbiBiZSB0dXJuZWQgdG8gMHMgYW5kIDFzIGluIHRoZSBmb2xsb3dpbmcgd2F5OiANCg0KYGBge3J9DQojIEJpbm9taWFsIGNhdGVnb3JpY2FsIHZhcmlhYmxlcyB0byAwIGFuZCAxDQp4JFBob25lU2VydmljZSA8LSBhcy5udW1lcmljKHgkUGhvbmVTZXJ2aWNlKS0xDQp4JFBhcnRuZXIgPC1hcy5udW1lcmljKHgkUGFydG5lciktMQ0KeCREZXBlbmRlbnRzIDwtIGFzLm51bWVyaWMoeCREZXBlbmRlbnRzKS0xDQp4JENodXJuIDwtIGFzLm51bWVyaWMoeCRDaHVybiktMQ0KeCRnZW5kZXIgPC0gYXMubnVtZXJpYyh4JGdlbmRlciktMQ0KYGBgDQoNCk5vdyBpdCBpcyBsZWZ0IHRvIGNvbnZlcnQgbXVsdGktY2xhc3MgdmFyaWFibGVzIDBzIGFuZCAxcy4gQmVmb3JlIHdlIGRvIGl0LCBsZXQncyBpbnNwZWN0IG91ciBkYXRhIGEgYml0Og0KDQpgYGB7cixldmFsPVRSVUV9DQojIyBDYXRlZ29yaWNhbCB2YXJpYWJsZXMgd2l0aCBtb3JlIHRoYW4gMiBjYXRlZ29yaWVzDQpwYXIobWZyb3c9Yyg1LDIpKQ0KIyBNdWx0aXBsZSBsaW5lcw0KZ2dwbG90KHgsIGFlcyhNdWx0aXBsZUxpbmVzLGZpbGw9TXVsdGlwbGVMaW5lcykpICsgDQogIGdlb21fYmFyKCkgKw0KICBsYWJzKHRpdGxlPSJNdWx0aXBsZSBsaW5lcyIseD0iIix5PSJDb3VudCIpDQoNCiMgSW50ZXJuZXQgc2VydmljZXMNCmdncGxvdCh4LCBhZXMoSW50ZXJuZXRTZXJ2aWNlLGZpbGw9SW50ZXJuZXRTZXJ2aWNlKSkgKyANCiAgZ2VvbV9iYXIoKSArDQogIGxhYnModGl0bGU9IkludGVybmV0IHNlcnZpY2UiLHg9IiIseT0iQ291bnQiKQ0KDQojIE9ubGluZVNlY3VyaXR5DQpnZ3Bsb3QoeCwgYWVzKE9ubGluZVNlY3VyaXR5LGZpbGw9T25saW5lU2VjdXJpdHkpKSArIA0KICBnZW9tX2JhcigpICsNCiAgbGFicyh0aXRsZT0iT25saW5lU2VjdXJpdHkiLHg9IiIseT0iQ291bnQiKQ0KDQojIE9ubGluZUJhY2t1cA0KZ2dwbG90KHgsIGFlcyhPbmxpbmVCYWNrdXAsZmlsbD1PbmxpbmVCYWNrdXApKSArIA0KICBnZW9tX2JhcigpICsNCiAgbGFicyh0aXRsZT0iT25saW5lQmFja3VwIix4PSIiLHk9IkNvdW50IikNCg0KIyBEZXZpY2VQcm90ZWN0aW9uDQpnZ3Bsb3QoeCwgYWVzKERldmljZVByb3RlY3Rpb24sZmlsbD1EZXZpY2VQcm90ZWN0aW9uKSkgKyANCiAgZ2VvbV9iYXIoKSArDQogIGxhYnModGl0bGU9IkRldmljZVByb3RlY3Rpb24iLHg9IiIseT0iQ291bnQiKQ0KDQojIFRlY2hTdXBwb3J0DQpnZ3Bsb3QoeCwgYWVzKFRlY2hTdXBwb3J0LGZpbGw9VGVjaFN1cHBvcnQpKSArIA0KICBnZW9tX2JhcigpICsNCiAgbGFicyh0aXRsZT0iVGVjaFN1cHBvcnQiLHg9IiIseT0iQ291bnQiKQ0KDQojIFN0cmVhbWluZ1RWDQpnZ3Bsb3QoeCwgYWVzKFN0cmVhbWluZ1RWLGZpbGw9U3RyZWFtaW5nVFYpKSArIA0KICBnZW9tX2JhcigpICsNCiAgbGFicyh0aXRsZT0iU3RyZWFtaW5nVFYiLHg9IiIseT0iQ291bnQiKQ0KDQojIFN0cmVhbWluZ01vdmllcw0KZ2dwbG90KHgsIGFlcyhTdHJlYW1pbmdNb3ZpZXMsZmlsbD1TdHJlYW1pbmdNb3ZpZXMpKSArIA0KICBnZW9tX2JhcigpICsNCiAgbGFicyh0aXRsZT0iU3RyZWFtaW5nTW92aWVzIix4PSIiLHk9IkNvdW50IikNCg0KIyBDb250cmFjdA0KZ2dwbG90KHgsIGFlcyhDb250cmFjdCxmaWxsPUNvbnRyYWN0KSkgKyANCiAgZ2VvbV9iYXIoKSArDQogIGxhYnModGl0bGU9IkNvbnRyYWN0Iix4PSIiLHk9IkNvdW50IikNCg0KIyBQYXltZW50TWV0aG9kDQpnZ3Bsb3QoeCwgYWVzKFBheW1lbnRNZXRob2QsZmlsbD1QYXltZW50TWV0aG9kKSkgKyANCiAgZ2VvbV9iYXIoKSArDQogIGxhYnModGl0bGU9IlBheW1lbnRNZXRob2QiLHg9IiIseT0iQ291bnQiKQ0KDQpgYGANCg0KDQpXZSBjYW4gYXBwbHkgb25lIGhvdCBlbmNvZGluZyB0byBvdXIgZGF0YSBzZXQgYnkgdXNpbmcgUidzIGJhc2UgZnVuY3Rpb24gYGBgbW9kZWwubWF0cml4YGBgLiBJbiB0aGUgY29kZSBiZWxvdywgfi4rMCBsZWFkcyB0byBlbmNvZGluZyBvZiBhbGwgY2F0ZWdvcmljYWwgdmFyaWFibGVzIHdpdGhvdXQgcHJvZHVjaW5nIGFuIGludGVyY2VwdC4NCg0KYGBge3J9DQojIE9uZS1ob3QgZW5jb2RpbmcNCngubWF0PC0gbW9kZWwubWF0cml4KH5NdWx0aXBsZUxpbmVzK0ludGVybmV0U2VydmljZStPbmxpbmVTZWN1cml0eStPbmxpbmVCYWNrdXArRGV2aWNlUHJvdGVjdGlvbitUZWNoU3VwcG9ydCtTdHJlYW1pbmdUVitDb250cmFjdCtTdHJlYW1pbmdNb3ZpZXMrUGFwZXJsZXNzQmlsbGluZytQYXltZW50TWV0aG9kKzAsZGF0YSA9IHgpDQpgYGANCg0KTm93IGlzIG91ciBkYXRhIHNldCBwcmUtcHJvY2Vzc2VkOg0KDQpgYGB7cn0NCiMgQ3JlYXRpb24gb2YgdGhlIGZpbmFsIGRhdGEgZnJhbWUgd2l0aCAwcyBhbmQgMXMNCmZpbmFsLmRmPC1hcy5kYXRhLmZyYW1lKHgubWF0KQ0KZmluYWwuZGY8LSBjYmluZCh4Lm1hdCx4JHRlbnVyZSx4JFRvdGFsQ2hhcmdlcyx4JE1vbnRobHlDaGFyZ2VzLHgkUGhvbmVTZXJ2aWNlLHgkUGFydG5lcix4JERlcGVuZGVudHMseCRnZW5kZXIseCRDaHVybikNCmNvbG5hbWVzKGZpbmFsLmRmKVsyNDozMV08LWMoInRlbnVyZSIsIlRvdGFsQ2hhcmdlcyIsIk1vbnRobHlDaGFyZ2VzIiwiUGhvbmVTZXJ2aWNlIiwiUGFydG5lciIsIkRlcGVuZGVudHMiLCJnZW5kZXIiLCJDaHVybiIpDQpgYGANCg0KIyMgRGF0YSBwYXJ0aXRpb24NCg0KTm93IHdlIG5lZWQgdG8gc3BsaXQgb3VyIGRhdGEgaW50byB0ZXN0IGFuZCB0cmFpbiBkYXRhLiBUaGUgcHJvcG9ydGlvbiBpcyA3MDozMC4NCg0KYGBge3J9DQojIENyZWF0aW9uIG9mIHRyYWluaW5nIGFuZCB0ZXN0IGRhdGEgc2V0cw0KaW5kZXggPC0gY2FyZXQ6OmNyZWF0ZURhdGFQYXJ0aXRpb24oeCRDaHVybiwgcCA9IDAuNywgbGlzdCA9IEYpDQp0cmFpbiA8LSBmaW5hbC5kZltpbmRleCxdDQp0ZXN0IDwtIGZpbmFsLmRmWy1pbmRleCxdDQoNCiMgUGFydGl0aW9uaW5nIHRlc3QgZGF0YQ0KeF90ZXN0IDwtIGFzLm1hdHJpeCh0ZXN0WywtMzFdKSAgDQp5X3Rlc3QgPC0gYXMubWF0cml4KHRlc3RbLDMxXSkNCg0KIyBQYXJ0aXRpb25pbmcgdHJhaW4gZGF0YQ0KeF90cmFpbiA8LSBhcy5tYXRyaXgodHJhaW5bLC0zMV0pDQp5X3RyYWluIDwtIGFzLm1hdHJpeCh0cmFpblssMzFdKQ0KYGBgDQoNCg0KVG8gdHJhaW4gb3VyIHJhbmRvbSBmb3Jlc3QgbW9kZWwgd2UgdXNlIGZ1bmN0aW9uIGByYW5kb21Gb3Jlc3QoKWAuIEF0IHRoaXMgcG9pbnQgd2Ugd2lsbCBub3QgYWltIHRvIGZpbmUgdHVuZSBvdXIgbW9kZWwsIHNvIHdlIHdpbGwgZGVmaW5lIGp1c3QgdHdvIHBhcmFtZXRlcnMsYG50cmVlYCBhbmQgYG1heG5vZGVzYDoNCg0KKiBgbnRyZWVgIGRlZmluZXMgdGhlIG51bWJlciBvZiB0cmVlcyB0byBidWlsZCBpbiB0aGUgZm9yZXN0Lg0KKiBgbWF4bm9kZXNgIGRlZmluZXMgdGhlIG1heGltdW0gbnVtYmVyIG9mIHRlcm1pbmFsIG5vZGVzIGVhY2ggdHJlZSBpbiB0aGUgZm9yZXN0IGNhbiBoYXZlLg0KDQojIyBSYW5kb20gRm9yZXN0IG1vZGVsDQoNCmBgYHtyfQ0KIyBSYW5kb20gRm9yZXN0DQojIFRyYWluaW5nDQpsaWJyYXJ5KHJhbmRvbUZvcmVzdCkNCnJmTW9kZWwgPC0gcmFuZG9tRm9yZXN0KHg9eF90cmFpbiwNCiAgICAgICAgICAgICAgICAgICAgICAgIHk9ZmFjdG9yKHlfdHJhaW4pLA0KICAgICAgICAgICAgICAgICAgICAgICAgbnRyZWU9NTAwLA0KICAgICAgICAgICAgICAgICAgICAgICAgbWF4bm9kZXM9MjQpDQpgYGANCg0KDQpTaW5jZSBwcmVkaWN0aW9ucyBhcmUgbWFkZSBiYXNlZCBvbiBmZWF0dXJlcyBvdXIgbW9kZWwgd2FzIHRyYWluZWQgb24sIGl0IGlzIHBvc3NpYmxlIHRvIG9ic2VydmUgaW1wb3J0YW5jZSBvZiBlYWNoIGZlYXR1cmUuIFRoZSBtb3JlIGltcG9ydGFudCBhIGZlYXR1cmUsIHRoZSBncmVhdGVyIGluZmx1ZW5jZSBpdCBleGVydHMgb24gcHJlZGljdGlvbnM6DQoNCmBgYHtyLGVycm9yPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KaW1wb3J0YW5jZV9mZWF0dXJlcyA8LSByYW5kb21Gb3Jlc3Q6OmltcG9ydGFuY2UocmZNb2RlbCkNCmltcG9ydGFuY2VfZmVhdHVyZXMgPC0gYXMuZGF0YS5mcmFtZShpbXBvcnRhbmNlX2ZlYXR1cmVzKQ0KaW1wb3J0YW5jZV9mZWF0dXJlcyRmZWF0dXJlcyA8LSByb3cubmFtZXMoaW1wb3J0YW5jZV9mZWF0dXJlcykNCmltcG9ydGFuY2VfZmVhdHVyZXMgPC0gaW1wb3J0YW5jZV9mZWF0dXJlc1tvcmRlcihpbXBvcnRhbmNlX2ZlYXR1cmVzJE1lYW5EZWNyZWFzZUdpbmkgLGRlY3JlYXNpbmcgPSBUUlVFKSxdDQoNCmxpYnJhcnkocGxvdGx5KQ0KcDwtZ2dwbG90KGltcG9ydGFuY2VfZmVhdHVyZXMpICsNCiAgZ2VvbV9wb2ludChhZXMocmVvcmRlcihmZWF0dXJlcyxNZWFuRGVjcmVhc2VHaW5pKSxNZWFuRGVjcmVhc2VHaW5pKSxzdGF0ID0gImlkZW50aXR5IikrDQogIHRoZW1lX21pbmltYWwoKSsNCiAgY29vcmRfZmxpcCgpKw0KICBsYWJzKHRpdGxlPSJJbXBvcnRhbnQgZmVhdHVyZXMiLHg9IkZlYXR1cmVzIikNCmdncGxvdGx5KHApDQpgYGANCg0KDQpBcyB3ZSBjYW4gc2VlIGZyb20gdGhpcyBvdXRwdXQsIHRoZSBgdGVudXJlYCBmZWF0dXJlIHNlZW1zIHRvIGJlIHRoZSBtb3N0IGltcG9ydGFudCBmYWN0b3IgaW4gbWFraW5nIHRoZSBmaW5hbCBwcmVkaWN0aW9uLiBGYWN0b3JzIHN1Y2ggYXMgYEludGVybmV0U2VydmljZUZpYmVyIG9wdGljYCxgVG90YWxDaGFyZ2VzYCBhbmQgYENvbnRyYWN0VHdvIHllYXJgIGNvbWUgc3Vic2VxdWVudGx5Lg0KDQoNCiMjIEV2YWx1YXRpbmcgTW9kZWwNCg0KSW4gb3JkZXIgdG8gZXZhbHVhdGUgb3VyIG1vZGVsIHdlIHdpbGwgdGFrZSBhIGxvb2sgYXQgYWNjdXJhY3ksIHByZWNpc2lvbiBhbmQgcmVjYWxsLg0KDQpgYGB7cn0NCiMgRXZhbHVhdGluZyBNb2RlbHMNCnByZWRpY3Rpb25faW5zYW1wbGUgPC0gYXMuZG91YmxlKHByZWRpY3QocmZNb2RlbCwgeF90cmFpbikpIC0gMQ0KcHJlZGljdGlvbl9vdXRzYW1wbGUgPC0gYXMuZG91YmxlKHByZWRpY3QocmZNb2RlbCwgeF90ZXN0KSkgLSAxDQpgYGANCg0KQWNjdXJhY3kgaXMgdGhlIHBlcmNlbnRhZ2Ugb2YgY29ycmVjdCBwcmVkaWN0aW9ucyBvdXQgb2YgYWxsIHByZWRpY3Rpb25zLg0KDQpgYGB7cn0NCiMgQWNjdXJhY3kNCmFjY3VfaW5zYW1wbGUgPC0gbWVhbih5X3RyYWluID09IHByZWRpY3Rpb25faW5zYW1wbGUpDQphY2N1X291dHNhbXBsZSA8LSBtZWFuKHlfdGVzdCA9PSBwcmVkaWN0aW9uX291dHNhbXBsZSkNCnByaW50KHNwcmludGYoJ0luLVNhbXBsZSBBY2N1cmFjeTogJTAuNGYnLCBhY2N1X2luc2FtcGxlKSkNCnByaW50KHNwcmludGYoJ091dC1TYW1wbGUgQWNjdXJhY3k6ICUwLjRmJywgYWNjdV9vdXRzYW1wbGUpKQ0KYGBgDQoNCldlIG1hbmFnZWQgdG8gYWNoaWV2ZSBwcmV0dHkgZ29vZCBvdXQtc2FtcGxlIGFjY3VyYWN5IGV2ZW4gd2l0aG91dCB0aG9yb3VnaCBmaW5lLXR1bmluZyBvdXIgcGFyYW1ldGVycy4NClByZWNpc2lvbiBpcyB0aGUgbnVtYmVyIG9mIHRydWUgcG9zaXRpdmVzIGRpdmlkZWQgYnkgdGhlIHRvdGFsIG51bWJlciBvZiB0cnVlIHBvc2l0aXZlcyBhbmQgZmFsc2UgcG9zaXRpdmVzLg0KDQpgYGB7cn0NCiMgUHJlY2lzaW9uDQpwcmVjX2luc2FtcGxlIDwtIHN1bShwcmVkaWN0aW9uX2luc2FtcGxlICYgeV90cmFpbikgLyBzdW0ocHJlZGljdGlvbl9pbnNhbXBsZSkNCnByZWNfb3V0c2FtcGxlIDwtIHN1bShwcmVkaWN0aW9uX291dHNhbXBsZSAmIHlfdGVzdCkgLyBzdW0ocHJlZGljdGlvbl9vdXRzYW1wbGUpDQpwcmludChzcHJpbnRmKCdJbi1TYW1wbGUgUHJlY2lzaW9uOiAlMC40ZicsIHByZWNfaW5zYW1wbGUpKQ0KcHJpbnQoc3ByaW50ZignT3V0LVNhbXBsZSBQcmVjaXNpb246ICUwLjRmJywgcHJlY19vdXRzYW1wbGUpKQ0KYGBgDQoNClJlY2FsbCBpcyBkZWZpbmVkIGFzIHRoZSBudW1iZXIgb2YgdHJ1ZSBwb3NpdGl2ZXMgZGl2aWRlZCBieSBudW1iZXIgb2YgdHJ1ZSBwb3NpdGl2ZXMgcGx1cyBmYWxzZSBuZWdhdGl2ZXMuIA0KDQpgYGB7cn0NCiMgUmVjYWxsIA0KcmVjYWxsX2luc2FtcGxlIDwtIHN1bShwcmVkaWN0aW9uX2luc2FtcGxlICYgeV90cmFpbikgLyBzdW0oeV90cmFpbikNCnJlY2FsbF9vdXRzYW1wbGUgPC0gc3VtKHByZWRpY3Rpb25fb3V0c2FtcGxlICYgeV90ZXN0KSAvIHN1bSh5X3Rlc3QpDQpwcmludChzcHJpbnRmKCdJbi1TYW1wbGUgUmVjYWxsOiAlMC40ZicsIHJlY2FsbF9pbnNhbXBsZSkpDQpwcmludChzcHJpbnRmKCdPdXQtU2FtcGxlIFJlY2FsbDogJTAuNGYnLCByZWNhbGxfb3V0c2FtcGxlKSkNCmBgYA0KDQoNCkZpbmFsbHksIGluIG9yZGVyIHRvIGVzdGltYXRlIGhvdyBnb29kIG91ciBtb2RlbCBpcyBpbiBjb21wYXJpc29uIHRvIHRoZSByYW5kb20gcHJlZGljdGlvbiwgd2Ugd2lsbCBpbnNwZWN0IFJPQyBjdXJ2ZSBhbmQgdGhlICoqQSoqcmVhICoqVSoqbmRlciB0aGUgKipDKip1cnZlLg0KDQpgYGB7cixlcnJvcj1GQUxTRSx3YXJuaW5nPUZBTFNFLGVycm9yPUZBTFNFfQ0KbGlicmFyeShST0NSKQ0KDQpwcmVkX3Byb2JfaW5zYW1wbGUgPC0gYXMuZG91YmxlKHByZWRpY3QocmZNb2RlbCwgeF90cmFpbiwgdHlwZT0ncHJvYicpWywyXSkNCnByZWRfcHJvYl9vdXRzYW1wbGUgPC0gYXMuZG91YmxlKHByZWRpY3QocmZNb2RlbCwgeF90ZXN0LCB0eXBlPSdwcm9iJylbLDJdKQ0KcHJlZCA8LSBwcmVkaWN0aW9uKHByZWRfcHJvYl9vdXRzYW1wbGUsIHlfdGVzdCkNCnBlcmYgPC0gcGVyZm9ybWFuY2UocHJlZCwgbWVhc3VyZSA9ICJ0cHIiLCB4Lm1lYXN1cmUgPSAiZnByIikgDQphdWMgPC0gcGVyZm9ybWFuY2UocHJlZCwgbWVhc3VyZT0nYXVjJylAeS52YWx1ZXNbWzFdXQ0KDQp7cGxvdChwZXJmLG1haW49c3ByaW50ZignUmFuZG9tIEZvcmVzdCAoQVVDOiAlMC4yZiknLCBhdWMpLGNvbD0nZGFya2JsdWUnLGx3ZD0yKSArIGdyaWQoKQ0KYWJsaW5lKGEgPSAwLCBiID0gMSwgY29sPSdkYXJrZ3JheScsIGx0eT00LCBsd2Q9Mil9DQpgYGANCg0KIyMgUmVmZXJlbmNlcw0KDQoqIEh3YW5nLCBZLiBILiAoMjAxOSkuIEhhbmRzLW9uIGRhdGEgc2NpZW5jZSBmb3IgbWFya2V0aW5nOiBJbXByb3ZlIHlvdXIgbWFya2V0aW5nIHN0cmF0ZWdpZXMgd2l0aCBtYWNoaW5lIGxlYXJuaW5nIHVzaW5nIFB5dGhvbiBhbmQgUi4gQmlybWluZ2hhbTogUGFja3QgUHVibGlzaGluZy4NCg0KKiBXLiBWZXJiZWtlLCBELiBNYXJ0ZW5zLCBDLiBNdWVzLCBCLiBCYWVzZW5zLiBCdWlsZGluZyBjb21wcmVoZW5zaWJsZSBjdXN0b21lciBjaHVybiBwcmVkaWN0aW9uIG1vZGVscyB3aXRoIGFkdmFuY2VkIHJ1bGUgaW5kdWN0aW9uIHRlY2huaXF1ZXMNCkV4cGVydCBTeXN0LiBBcHBsLiwgMzggKDMpICgyMDExKSwgcHAuIDIzNTQtMjM2NC4NCg0KKiBNLlIuIENvbGdhdGUsIFAuSi4gRGFuYWhlci4gSW1wbGVtZW50aW5nIGEgY3VzdG9tZXIgcmVsYXRpb25zaGlwIHN0cmF0ZWd5OiB0aGUgYXN5bW1ldHJpYyBpbXBhY3Qgb2YgcG9vciB2ZXJzdXMgZXhjZWxsZW50IGV4ZWN1dGlvbg0KSi4gQWNhZC4gTWFyay4gU2NpLiwgMjggKDMpICgyMDAwKSwgcHAuIDM3NS0zODcuDQoNCiogSi4gR2FuZXNoLCBNLkouIEFybm9sZCwgSy5FLiBSZXlub2xkcy4gVW5kZXJzdGFuZGluZyB0aGUgY3VzdG9tZXIgYmFzZSBvZiBzZXJ2aWNlIHByb3ZpZGVyczogYW4gZXhhbWluYXRpb24gb2YgdGhlIGRpZmZlcmVuY2VzIGJldHdlZW4gc3dpdGNoZXJzIGFuZCBzdGF5ZXJzLkouIE1hcmsuLCA2NCAoMykgKDIwMDApLCBwcC4gNjUtODcuDQoNCiogRC5WLiBkZW4gUG9lbCwgQi4gTGFyaXZpw6hyZS4gQ3VzdG9tZXIgYXR0cml0aW9uIGFuYWx5c2lzIGZvciBmaW5hbmNpYWwgc2VydmljZXMgdXNpbmcgcHJvcG9ydGlvbmFsIGhhemFyZCBtb2RlbHMuIEV1ci4gSi4gT3Blci4gUmVzLiwgMTU3ICgxKSAoMjAwNCksIHBwLiAxOTYtMjE3Lg0K